package kom;

import java.awt.Color;
import java.awt.Rectangle;
import java.awt.geom.Point2D;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import kom.Effects.AnimatedEffect;
import kom.Effects.EffectType;
import kom.gfx.Screen;
import kom.sound.Audio;

public final class Map {

    private final int off = 4;
    private HashMap<String, Rectangle> dRects = new HashMap<String, Rectangle>();
    private Block[][] map;
    private int w;
    private int h;
    //map stuff
    public static Effects mapEffects = new Effects();
    private List<Player> players = new LinkedList<Player>();
    //private ThreadSafeList<ItemDrop> itemDrops = new ThreadSafeList<ItemDrop>();

    public void setTile(Point2D tile, BlockType block) {
        map[(int) tile.getX()][(int) tile.getY()].setBlockType(block);
        calculateTileBorders();
    }

    public Block getTile(Point2D tile) {
        try {
            return map[(int) tile.getX()][(int) tile.getY()];
        } catch(Throwable t) { return null;}
    }

    public void teleportEntity(LivingEntity e, int x, int y) {
        e.setX(x * 8);
        e.setY(y * 8);
    }

    public void addPlayer(Player p) {
        players.add(p);
        teleportEntity(p, 2, 2);
    }

    // creating a testmap
    public Map(int s) {
        this.w = s + off;
        this.h = s + off;
        map = new Block[w][h];
        for (int x = 0; x < w; x++) {
            for (int y = 0; y < h; y++) {
                if (y == h - 1) {
                    setTile(x, y, BlockType.STONE);
                } else if (x < 2 || x > w - 3 || y < 2 || y > h - 3) {
                    setTile(x, y, BlockType.DIRT);
                } else {
                    setTile(x, y, BlockType.AIR);
                }
            }
        }
//        ItemDrop pick = new ItemDrop(ItemType.PICKAXE);
//        pick.x = 70;
//        pick.y = 25;
//        itemDrops.add(pick);
        calculateTileBorders();
//        itemDrops.save();
    }

    public void setTile(int x, int y, BlockType b) {
        map[x][y] = new Block(b);
    }
    // to make thread safe since ticks run 'inside' renders tining
    // public boolean readBuffer = false;
    // private List<Border> tileBordersBuffer = new LinkedList<Border>();
    // private List<Border> tileBorders = new LinkedList<Border>();
    private ThreadSafeList<Border> tileBorders = new ThreadSafeList<Border>();

    // make tiles more cooler by having borders, not being a flat dull square
    public void calculateTileBorders() {
        for (int x = 0; x < w; x++) {
            for (int y = 0; y < h; y++) {
                if (x == w - 1 || map[x][y].getBlockType().getPriorioty() > map[x + 1][y].getBlockType().getPriorioty()) {
                    tileBorders.add(new Border(x + 1, y, Map.DIR_RIGHT, map[x][y].getBlockType()));
                }
                if (x == 0 || map[x][y].getBlockType().getPriorioty() > map[x - 1][y].getBlockType().getPriorioty()) {
                    tileBorders.add(new Border(x - 1, y, Map.DIR_LEFT, map[x][y].getBlockType()));
                }
                if (y == 0 || map[x][y].getBlockType().getPriorioty() > map[x][y - 1].getBlockType().getPriorioty()) {
                    tileBorders.add(new Border(x, y - 1, Map.DIR_DOWN, map[x][y].getBlockType()));
                }
                if (y == h - 1 || map[x][y].getBlockType().getPriorioty() > map[x][y + 1].getBlockType().getPriorioty()) {
                    tileBorders.add(new Border(x, y + 1, Map.DIR_UP, map[x][y].getBlockType()));
                }
            }
        }
        tileBorders.save();
    }

    public Rectangle debugPoint(Point2D p) {
        return new Rectangle((int) p.getX(), (int) p.getY(), 2, 2);
    }

    public boolean isPassable(Block tile) {
        if (tile.getBlockType() == BlockType.AIR) {
            return true;
        } else {
            return false;
        }
    }

    // gets the collision box of a X Y tile (not XY pixel)
    private Rectangle getBoxFromTile(int x, int y) {
        if (x > this.w) {
            x = w;
        }
        if (y > this.h) {
            y = h;
        }
        Rectangle r = new Rectangle((x * 8), (y * 8), 8, 8);
        return r;
    }

    public Rectangle getBoxFromTile(Point2D tile) {
        return new Rectangle((int) (tile.getX()) * 8, (int) (tile.getY()) * 8, 8, 8);
    }

    // returns witch TILE X Y the entity is not pixel X Y
    public Point2D getTileEntityIs(LivingEntity p) {
        Point2D pt = new Point2D.Float();
        pt.setLocation(((p.getX()) / 8), ((p.getY()) / 8) + 1);
        return pt;
    }

    // tells you witch tile that pixel is. This pixel has already to be adjusted in cameraX and cameraY
    // such as the entityX Y
    public Point2D getTilePointIs(Point2D p) {
        Point2D pt = new Point2D.Float();
        //  pt.setLocation((int)(p.getX()+cameraX) / 8,(int)(p.getY()+cameraY) / 8);
        pt.setLocation(((int) p.getX()) / 8, ((int) p.getY()) / 8);
        return pt;
    }

    public void checkIfEntityShouldFall(LivingEntity p) {
        try {

            // if its falling we gonna check if there are blocks on its feet so it can stop falling
            if (p.isApplyGravity()) {
                // gets the exact point where player feets are
                // these values are ALREADY ADJUSTED by cameraX and cameraY
                Point2D rightFeet = new Point2D.Double(p.getFutureBox().getX() + p.getBox().getWidth() - 4, p.getBox().getY() + p.getBox().getHeight() + 2 + p.getAceleration().y);
                Point2D leftFeet = new Point2D.Double(p.getFutureBox().getX() + 3, p.getBox().getY() + p.getBox().getHeight() + 2 + p.getAceleration().y);

                // gets the tile under those feets
                Point2D stepRight = getTilePointIs(rightFeet);
                Point2D stepLeft = getTilePointIs(leftFeet);

//                dRects.put("rfeet", getBoxFromTile(stepRight));
//                dRects.put("lfeet", getBoxFromTile(stepLeft));

                if (getTile(stepLeft).getBlockType() == BlockType.AIR && getTile(stepRight).getBlockType() == BlockType.AIR) {
                    p.setApplyGravity(true);
                } else {
                    //if (getTile(upStepRight) == BlockType.AIR && getTile(upStepLeft) == BlockType.AIR) {
                    if (p.isFalling()) {
                        AnimatedEffect eff = mapEffects.getNewEffect((int) p.getFutureBox().x, (int) stepRight.getY() * 8 - 10, EffectType.SMOKE, 0, 0);
                        eff.maxFrame = 6;
                        eff.ticksToFrame = 2;
                        mapEffects.playEffect(eff);
                        Audio.playSound("hitfloor");
                    }
                    p.setApplyGravity(false);
                    p.y = (float) stepRight.getY() * 8 - 16;
                    //}
                    // }
                }
            }

        } catch (Throwable t) {
            t.printStackTrace(System.out);
            p.setApplyGravity(false);
        }
    }

    public void checkHeadHit(LivingEntity p) {
        try {

            // if the player is going up

            if (p.getAceleration().y < 0) {
                // gets the exact point where player feets are
                // these values are ALREADY ADJUSTED by cameraX and cameraY
                Point2D rightFeet = new Point2D.Double(p.getBox().getX() + p.getBox().getWidth() - 4, p.getBox().getY() + p.getBox().getHeight() + 2 + p.getAceleration().y);
                Point2D leftFeet = new Point2D.Double(p.getBox().getX() + 3, p.getBox().getY() + p.getBox().getHeight() + 2 + p.getAceleration().y);

                // gets the tile under those feets
                Point2D stepRight = getTilePointIs(rightFeet);
                Point2D stepLeft = getTilePointIs(leftFeet);

                stepRight.setLocation(stepRight.getX(), stepRight.getY() - 2);
                stepLeft.setLocation(stepLeft.getX(), stepLeft.getY() - 2);
                //dRects.put("head1", getBoxFromTile(stepRight));
                //dRects.put("head2", getBoxFromTile(stepLeft));
                // no passable tiles , player hits his head
                if (getTile(stepLeft).getBlockType() != BlockType.AIR || getTile(stepRight).getBlockType() != BlockType.AIR) {
                    // if he hits his head strongly
                    if (p.getAceleration().y < -3) {
                        AnimatedEffect eff = mapEffects.getNewEffect((int) p.getFutureBox().x, (int) stepRight.getY() * 8 - 10, EffectType.SPARK, 0, +8);
                        eff.maxFrame = 6;
                        eff.ticksToFrame = 1;
                        if (p.getAceleration().x > 0) {
                            eff.aceleration.x = -1;
                        } else {
                            eff.aceleration.x = 1;
                        }
                        eff.aceleration.y = 2;
                        eff.rewind = true;
                        mapEffects.playEffect(eff);
                    }

                    p.setCanJump(false);
                    p.setJumping(false);
                    p.setApplyGravity(true);
                    p.getAceleration().y = 0;
                }
            }

        } catch (Throwable t) {
            t.printStackTrace(System.out);
            p.setApplyGravity(false);
        }
    }

    public void tick() {
        for (Player p : players) {
            checkIfEntityShouldFall(p);
            checkHeadHit(p);
            // tick gravity
            p.gravityTick();
            // tick the animation
            p.tick();
            // gets the new acelerations based on its direction
            p.calculateNextPosition();

            // get colidables near the character
            HashSet collisions = this.getEntityColisions(p);
            if (collisions.contains(Map.DIR_RIGHT) && p.getAceleration().x > 0) {
                p.hitWall(Map.DIR_RIGHT);
            } else if (collisions.contains(Map.DIR_LEFT) && p.getAceleration().x < 0) {
                p.hitWall(Map.DIR_LEFT);
            }
            p.positionTick();

        }

//        for (ItemDrop i : itemDrops.getList()) {
//            i.gravityTick();
//            i.positionTick();
//            Point2D tile = this.getTilePointIs(new Point2D.Float(i.x, i.y));
//            if (!isPassable(getTile(tile))) {

//                i.y = (int) (tile.getY() - 1) * 8;
//                i.applyGravity = false;

        /////// GOTTA KNOW WHY IS THIS HAPPENING !!!
        // MADE ME NUTZ @_@
        // ANIMATION DOESNT GET OFF THE SCREEN
                /*
         AnimatedEffect eff = mapEffects.getNewEffect((int) i.x+7, (int) i.y, EffectType.SPARK, 0, 0);
         eff.maxFrame = 4;
         eff.rewind = true;
         eff.ticksToFrame = 2;
         mapEffects.playEffect(eff);
         * 
         */
//            }
//        }

        mapEffects.tick();
    }

    // force something to try to move (such as the player)
    public void forceMoveEntity(LivingEntity e, int direction) {
        if (e instanceof Player) {
            Player p = players.get(players.indexOf(e));
            if (!e.isStunned()) {
                p.setDirection(direction);
            }
        }
    }

    // this is confusing lol, it should tell whenever a player can move
    // returns a HashSet<Integer> with directions that are blocked
    // i may want to otimize this later
    public HashSet<Integer> getEntityColisions(LivingEntity p) {
        int tryDir = 0;
        HashSet<Integer> collisions = new HashSet<Integer>();
        try {
            // running right
            // get tile where player is (the center tile , player is a 2x2, the left-bottom tile its his 'center tile'
            Point2D playerTile = this.getTileEntityIs(p);
            // this is to know witch dir im tryng to use it in Catch
            tryDir = Map.DIR_RIGHT;
            // get the 2 tiles at player right >>
            Block next1 = map[(int) playerTile.getX() + 2][(int) playerTile.getY() - 1];
            Block next2 = map[(int) playerTile.getX() + 2][(int) playerTile.getY()];

            // make a collision box of those (for now it only makes 1 tile)
            Rectangle colision = getBoxFromTile((int) playerTile.getX() + 2, (int) playerTile.getY() - 1);
            colision.setSize(8, 16);
            // debug3 = colision;
            if ((!isPassable(next1) || !isPassable(next2)) && p.getFutureBox().intersects(colision)) {
                // collided to the right
                //p.setFalling(false);
                collisions.add(Map.DIR_RIGHT);
            }
            // this is to know witch dir im tryng to use it in Catch
            tryDir = Map.DIR_LEFT;
            Point2D playerTile2 = this.getTileEntityIs(p);
            Block next12 = map[(int) playerTile2.getX()][(int) playerTile2.getY() - 1];
            Block next22 = map[(int) playerTile2.getX()][(int) playerTile2.getY()];
            Rectangle colision2 = getBoxFromTile((int) playerTile2.getX(), (int) playerTile2.getY() - 1);
            colision2.setSize(8, 16);
            //  debug4 = colision2;
            if ((!isPassable(next12) || !isPassable(next22)) && p.getFutureBox().intersects(colision2)) {
                collisions.add(Map.DIR_LEFT);
            }
        } catch (Throwable t) {
            t.printStackTrace(System.out);
            // ill see this later, for now this works lol
            collisions.add(tryDir);
        }
        // no colision
        return collisions;
    }

    public void render(Screen screen) {
        renderMap(screen);
        renderBorders(screen);
        renderPlayers(screen);
        renderItems(screen);

        mapEffects.renderEffects(screen);
    }

    public void renderItems(Screen screen) {
//        for (ItemDrop i : itemDrops.getList()) {
//            g.drawImage(bits.iconSet[i.getItemType().id], (int) i.x - xoffs, (int) i.y - yoffs, null);
//        }
    }

    public void renderBorders(Screen screen) {
        for (Border b : tileBorders.getList()) {
            screen.drawBorder(b);
        }
    }

    // draw the map tiles and debug stuff
    public void renderMap(Screen screen) {
        screen.drawMap(w, h, map);
        
        for (Rectangle rect : dRects.values()) {
            screen.drawRect(rect, true, Color.BLACK);
        }

    }

    public void renderPlayers(Screen screen) {
        for (Player p : players) {
            if (p.getFacing() == 0) {
                p.setFacing(p.getDirection());
            }
            screen.drawPlayer(p);
        }
    }

    public int getH() {
        return h;
    }

    public void setH(int h) {
        this.h = h;
    }

    public int getW() {
        return w;
    }

    public void setW(int w) {
        this.w = w;
    }
    public final static int DIR_UP = 8;
    public final static int DIR_LEFT = 4;
    public final static int DIR_RIGHT = 6;
    public final static int DIR_DOWN = 2;
    public final static int DIR_NONE = 0;
}
